/*
# Copyright IBM Corp. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
*/

// ====CHAINCODE EXECUTION SAMPLES (CLI) ==================

// ==== Invoke marbles ====
// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["initMarble","marble1","blue","35","tom"]}'
// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["initMarble","marble2","red","50","tom"]}'
// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["initMarble","marble3","blue","70","tom"]}'
// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["transferMarble","marble2","jerry"]}'
// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["transferMarblesBasedOnColor","blue","jerry"]}'
// peer chaincode invoke -C myc1 -n marbles -c '{"Args":["delete","marble1"]}'

// ==== Query marbles ====
// peer chaincode query -C myc1 -n marbles -c '{"Args":["readMarble","marble1"]}'
// peer chaincode query -C myc1 -n marbles -c '{"Args":["getMarblesByRange","marble1","marble3"]}'
// peer chaincode query -C myc1 -n marbles -c '{"Args":["getHistoryForMarble","marble1"]}'
// peer chaincode query -C myc1 -n marbles -c '{"Args":["getMarblesByRangeWithPagination","marble1","marble3","3",""]}'

// Rich Query (Only supported if CouchDB is used as state database):
// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarblesByOwner","tom"]}'
// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarbles","{\"selector\":{\"owner\":\"tom\"}}"]}'

// Rich Query with Pagination (Only supported if CouchDB is used as state database):
// peer chaincode query -C myc1 -n marbles -c '{"Args":["queryMarblesWithPagination","{\"selector\":{\"owner\":\"tom\"}}","3",""]}'

'use strict';
const shim = require('fabric-shim');
const util = require('util');

let Chaincode = class {
	async Init(stub) {
		let ret = stub.getFunctionAndParameters();
		console.info(ret);
		console.info('=========== Instantiated Marbles Chaincode ===========');
		return shim.success();
	}

	async Invoke(stub) {
		console.info('Transaction ID: ' + stub.getTxID());
		console.info(util.format('Args: %j', stub.getArgs()));

		let ret = stub.getFunctionAndParameters();
		console.info(ret);

		let method = this[ret.fcn];
		if (!method) {
			console.log('no function of name:' + ret.fcn + ' found');
			throw new Error('Received unknown function ' + ret.fcn + ' invocation');
		}
		try {
			let payload = await method(stub, ret.params, this);
			return shim.success(payload);
		} catch (err) {
			console.log(err);
			return shim.error(err);
		}
	}

	// ===============================================
	// initMarble - create a new marble
	// ===============================================
	async initMarble(stub, args, thisClass) {
		if (args.length != 4) {
			throw new Error('Incorrect number of arguments. Expecting 4');
		}
		// ==== Input sanitation ====
		console.info('--- start init marble ---');
		if (args[0].lenth <= 0) {
			throw new Error('1st argument must be a non-empty string');
		}
		if (args[1].lenth <= 0) {
			throw new Error('2nd argument must be a non-empty string');
		}
		if (args[2].lenth <= 0) {
			throw new Error('3rd argument must be a non-empty string');
		}
		if (args[3].lenth <= 0) {
			throw new Error('4th argument must be a non-empty string');
		}
		let marbleName = args[0];
		let color = args[1].toLowerCase();
		let owner = args[3].toLowerCase();
		let size = parseInt(args[2]);
		if (typeof size !== 'number') {
			throw new Error('3rd argument must be a numeric string');
		}

		// ==== Check if marble already exists ====
		let marbleState = await stub.getState(marbleName);
		if (marbleState.toString()) {
			throw new Error('This marble already exists: ' + marbleName);
		}

		// ==== Create marble object and marshal to JSON ====
		let marble = {};
		marble.docType = 'marble';
		marble.name = marbleName;
		marble.color = color;
		marble.size = size;
		marble.owner = owner;

		// === Save marble to state ===
		await stub.putState(marbleName, Buffer.from(JSON.stringify(marble)));
		let indexName = 'color~name';
		let colorNameIndexKey = await stub.createCompositeKey(indexName, [
			marble.color,
			marble.name
		]);
		console.info(colorNameIndexKey);
		//  Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble.
		//  Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value
		await stub.putState(colorNameIndexKey, Buffer.from('\u0000'));
		// ==== Marble saved and indexed. Return success ====
		console.info('- end init marble');
	}

	// ===============================================
	// readMarble - read a marble from chaincode state
	// ===============================================
	async readMarble(stub, args, thisClass) {
		if (args.length != 1) {
			throw new Error(
				'Incorrect number of arguments. Expecting name of the marble to query'
			);
		}

		let name = args[0];
		if (!name) {
			throw new Error(' marble name must not be empty');
		}
		let marbleAsbytes = await stub.getState(name); //get the marble from chaincode state
		if (!marbleAsbytes.toString()) {
			let jsonResp = {};
			jsonResp.Error = 'Marble does not exist: ' + name;
			throw new Error(JSON.stringify(jsonResp));
		}
		console.info('=======================================');
		console.log(marbleAsbytes.toString());
		console.info('=======================================');
		return marbleAsbytes;
	}

	// ==================================================
	// delete - remove a marble key/value pair from state
	// ==================================================
	async delete(stub, args, thisClass) {
		if (args.length != 1) {
			throw new Error(
				'Incorrect number of arguments. Expecting name of the marble to delete'
			);
		}
		let marbleName = args[0];
		if (!marbleName) {
			throw new Error('marble name must not be empty');
		}
		// to maintain the color~name index, we need to read the marble first and get its color
		let valAsbytes = await stub.getState(marbleName); //get the marble from chaincode state
		let jsonResp = {};
		if (!valAsbytes) {
			jsonResp.error = 'marble does not exist: ' + name;
			throw new Error(jsonResp);
		}
		let marbleJSON = {};
		try {
			marbleJSON = JSON.parse(valAsbytes.toString());
		} catch (err) {
			jsonResp = {};
			jsonResp.error = 'Failed to decode JSON of: ' + marbleName;
			throw new Error(jsonResp);
		}

		await stub.deleteState(marbleName); //remove the marble from chaincode state

		// delete the index
		let indexName = 'color~name';
		let colorNameIndexKey = stub.createCompositeKey(indexName, [
			marbleJSON.color,
			marbleJSON.name
		]);
		if (!colorNameIndexKey) {
			throw new Error(' Failed to create the createCompositeKey');
		}
		//  Delete index entry to state.
		await stub.deleteState(colorNameIndexKey);
	}

	// ===========================================================
	// transfer a marble by setting a new owner name on the marble
	// ===========================================================
	async transferMarble(stub, args, thisClass) {
		//   0       1
		// 'name', 'bob'
		if (args.length < 2) {
			throw new Error(
				'Incorrect number of arguments. Expecting marblename and owner'
			);
		}

		let marbleName = args[0];
		let newOwner = args[1].toLowerCase();
		console.info('- start transferMarble ', marbleName, newOwner);

		let marbleAsBytes = await stub.getState(marbleName);
		if (!marbleAsBytes || !marbleAsBytes.toString()) {
			throw new Error('marble does not exist');
		}
		let marbleToTransfer = {};
		try {
			marbleToTransfer = JSON.parse(marbleAsBytes.toString()); //unmarshal
		} catch (err) {
			let jsonResp = {};
			jsonResp.error = 'Failed to decode JSON of: ' + marbleName;
			throw new Error(jsonResp);
		}
		console.info(marbleToTransfer);
		marbleToTransfer.owner = newOwner; //change the owner

		let marbleJSONasBytes = Buffer.from(JSON.stringify(marbleToTransfer));
		await stub.putState(marbleName, marbleJSONasBytes); //rewrite the marble

		console.info('- end transferMarble (success)');
	}

	// ===========================================================================================
	// getMarblesByRange performs a range query based on the start and end keys provided.

	// Read-only function results are not typically submitted to ordering. If the read-only
	// results are submitted to ordering, or if the query is used in an update transaction
	// and submitted to ordering, then the committing peers will re-execute to guarantee that
	// result sets are stable between endorsement time and commit time. The transaction is
	// invalidated by the committing peers if the result set has changed between endorsement
	// time and commit time.
	// Therefore, range queries are a safe option for performing update transactions based on query results.
	// ===========================================================================================
	async getMarblesByRange(stub, args, thisClass) {
		if (args.length < 2) {
			throw new Error('Incorrect number of arguments. Expecting 2');
		}

		let startKey = args[0];
		let endKey = args[1];

		let resultsIterator = await stub.getStateByRange(startKey, endKey);
		let method = thisClass['getAllResults'];
		let results = await method(resultsIterator, false);

		return Buffer.from(JSON.stringify(results));
	}

	// ==== Example: GetStateByPartialCompositeKey/RangeQuery =========================================
	// transferMarblesBasedOnColor will transfer marbles of a given color to a certain new owner.
	// Uses a GetStateByPartialCompositeKey (range query) against color~name 'index'.
	// Committing peers will re-execute range queries to guarantee that result sets are stable
	// between endorsement time and commit time. The transaction is invalidated by the
	// committing peers if the result set has changed between endorsement time and commit time.
	// Therefore, range queries are a safe option for performing update transactions based on query results.
	// ===========================================================================================
	async transferMarblesBasedOnColor(stub, args, thisClass) {
		//   0       1
		// 'color', 'bob'
		if (args.length < 2) {
			throw new Error('Incorrect number of arguments. Expecting color and owner');
		}

		let color = args[0];
		let newOwner = args[1].toLowerCase();
		console.info('- start transferMarblesBasedOnColor ', color, newOwner);

		// Query the color~name index by color
		// This will execute a key range query on all keys starting with 'color'
		let coloredMarbleResultsIterator = await stub.getStateByPartialCompositeKey(
			'color~name',
			[color]
		);

		let method = thisClass['transferMarble'];
		// Iterate through result set and for each marble found, transfer to newOwner
		while (true) {
			let responseRange = await coloredMarbleResultsIterator.next();
			if (!responseRange || !responseRange.value || !responseRange.value.key) {
				return;
			}
			console.log(responseRange.value.key);

			// let value = res.value.value.toString('utf8');
			let objectType;
			let attributes;
			({ objectType, attributes } = await stub.splitCompositeKey(
				responseRange.value.key
			));

			let returnedColor = attributes[0];
			let returnedMarbleName = attributes[1];
			console.info(
				util.format(
					'- found a marble from index:%s color:%s name:%s\n',
					objectType,
					returnedColor,
					returnedMarbleName
				)
			);

			// Now call the transfer function for the found marble.
			// Re-use the same function that is used to transfer individual marbles
			let response = await method(stub, [returnedMarbleName, newOwner]);
		}

		let responsePayload = util.format(
			'Transferred %s marbles to %s',
			color,
			newOwner
		);
		console.info('- end transferMarblesBasedOnColor: ' + responsePayload);
	}

	// ===== Example: Parameterized rich query =================================================
	// queryMarblesByOwner queries for marbles based on a passed in owner.
	// This is an example of a parameterized query where the query logic is baked into the chaincode,
	// and accepting a single query parameter (owner).
	// Only available on state databases that support rich query (e.g. CouchDB)
	// =========================================================================================
	async queryMarblesByOwner(stub, args, thisClass) {
		//   0
		// 'bob'
		if (args.length < 1) {
			throw new Error('Incorrect number of arguments. Expecting owner name.');
		}

		let owner = args[0].toLowerCase();
		let queryString = {};
		queryString.selector = {};
		queryString.selector.docType = 'marble';
		queryString.selector.owner = owner;
		let method = thisClass['getQueryResultForQueryString'];
		let queryResults = await method(stub, JSON.stringify(queryString), thisClass);
		return queryResults; //shim.success(queryResults);
	}

	// ===== Example: Ad hoc rich query ========================================================
	// queryMarbles uses a query string to perform a query for marbles.
	// Query string matching state database syntax is passed in and executed as is.
	// Supports ad hoc queries that can be defined at runtime by the client.
	// If this is not desired, follow the queryMarblesForOwner example for parameterized queries.
	// Only available on state databases that support rich query (e.g. CouchDB)
	// =========================================================================================
	async queryMarbles(stub, args, thisClass) {
		//   0
		// 'queryString'
		if (args.length < 1) {
			throw new Error('Incorrect number of arguments. Expecting queryString');
		}
		let queryString = args[0];
		if (!queryString) {
			throw new Error('queryString must not be empty');
		}
		let method = thisClass['getQueryResultForQueryString'];
		let queryResults = await method(stub, queryString, thisClass);
		return queryResults;
	}

	async getAllResults(iterator, isHistory) {
		let allResults = [];
		while (true) {
			let res = await iterator.next();

			if (res.value && res.value.value.toString()) {
				let jsonRes = {};
				console.log(res.value.value.toString('utf8'));

				if (isHistory && isHistory === true) {
					jsonRes.TxId = res.value.tx_id;
					jsonRes.Timestamp = res.value.timestamp;
					jsonRes.IsDelete = res.value.is_delete.toString();
					try {
						jsonRes.Value = JSON.parse(res.value.value.toString('utf8'));
					} catch (err) {
						console.log(err);
						jsonRes.Value = res.value.value.toString('utf8');
					}
				} else {
					jsonRes.Key = res.value.key;
					try {
						jsonRes.Record = JSON.parse(res.value.value.toString('utf8'));
					} catch (err) {
						console.log(err);
						jsonRes.Record = res.value.value.toString('utf8');
					}
				}
				allResults.push(jsonRes);
			}
			if (res.done) {
				console.log('end of data');
				await iterator.close();
				console.info(allResults);
				return allResults;
			}
		}
	}

	// =========================================================================================
	// getQueryResultForQueryString executes the passed in query string.
	// Result set is built and returned as a byte array containing the JSON results.
	// =========================================================================================
	async getQueryResultForQueryString(stub, queryString, thisClass) {
		console.info('- getQueryResultForQueryString queryString:\n' + queryString);
		let resultsIterator = await stub.getQueryResult(queryString);
		let method = thisClass['getAllResults'];

		let results = await method(resultsIterator, false);

		return Buffer.from(JSON.stringify(results));
	}

	async getHistoryForMarble(stub, args, thisClass) {
		if (args.length < 1) {
			throw new Error('Incorrect number of arguments. Expecting 1');
		}
		let marbleName = args[0];
		console.info('- start getHistoryForMarble: %s\n', marbleName);

		let resultsIterator = await stub.getHistoryForKey(marbleName);
		let method = thisClass['getAllResults'];
		let results = await method(resultsIterator, true);

		return Buffer.from(JSON.stringify(results));
	}

	// ====== Pagination =========================================================================
	// Pagination provides a method to retrieve records with a defined pagesize and
	// start point (bookmark).  An empty string bookmark defines the first "page" of a query
	// result. Paginated queries return a bookmark that can be used in
	// the next query to retrieve the next page of results. Paginated queries extend
	// rich queries and range queries to include a pagesize and bookmark.
	//
	// Two examples are provided in this example. The first is getMarblesByRangeWithPagination
	// which executes a paginated range query.
	// The second example is a paginated query for rich ad-hoc queries.
	// =========================================================================================

	// ====== Example: Pagination with Range Query ===============================================
	// getMarblesByRangeWithPagination performs a range query based on the start & end key,
	// page size and a bookmark.
	//
	// The number of fetched records will be equal to or lesser than the page size.
	// Paginated range queries are only valid for read only transactions.
	// ===========================================================================================
	async getMarblesByRangeWithPagination(stub, args, thisClass) {
		if (args.length < 2) {
			throw new Error('Incorrect number of arguments. Expecting 2');
		}
		const startKey = args[0];
		const endKey = args[1];

		const pageSize = parseInt(args[2], 10);
		const bookmark = args[3];

		const { iterator, metadata } = await stub.getStateByRangeWithPagination(
			startKey,
			endKey,
			pageSize,
			bookmark
		);
		const getAllResults = thisClass['getAllResults'];
		const results = await getAllResults(iterator, false);
		// use RecordsCount and Bookmark to keep consistency with the go sample
		results.ResponseMetadata = {
			RecordsCount: metadata.fetched_records_count,
			Bookmark: metadata.bookmark
		};
		return Buffer.from(JSON.stringify(results));
	}

	// =========================================================================================
	// getQueryResultForQueryStringWithPagination executes the passed in query string with
	// pagination info. Result set is built and returned as a byte array containing the JSON results.
	// =========================================================================================
	async queryMarblesWithPagination(stub, args, thisClass) {
		//   0
		// "queryString"
		if (args.length < 3) {
			return shim.Error('Incorrect number of arguments. Expecting 3');
		}

		const queryString = args[0];
		const pageSize = parseInt(args[2], 10);
		const bookmark = args[3];

		const { iterator, metadata } = await stub.GetQueryResultWithPagination(
			queryString,
			pageSize,
			bookmark
		);
		const getAllResults = thisClass['getAllResults'];
		const results = await getAllResults(iterator, false);
		// use RecordsCount and Bookmark to keep consistency with the go sample
		results.ResponseMetadata = {
			RecordsCount: metadata.fetched_records_count,
			Bookmark: metadata.bookmark
		};

		return Buffer.from(JSON.stringify(results));
	}
};

shim.start(new Chaincode());
